package pl.edu.agh.ki.neuralnetwork.builder;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import java.util.Random;

import pl.edu.agh.ki.neuralnetwork.exceptions.NeuronNotConnectedException;
import pl.edu.agh.ki.neuralnetwork.exceptions.WrongArgException;
import pl.edu.agh.ki.neuralnetwork.layer.GrossbergLayer;
import pl.edu.agh.ki.neuralnetwork.layer.KohonenLayer;
import pl.edu.agh.ki.neuralnetwork.layer.Layer;
import pl.edu.agh.ki.neuralnetwork.layer.SimpleLayer;
import pl.edu.agh.ki.neuralnetwork.network.NeuralNetwork;
import pl.edu.agh.ki.neuralnetwork.network.NeuralNetworkImpl;
import pl.edu.agh.ki.neuralnetwork.neurons.GrossbergLinearNeuron;
import pl.edu.agh.ki.neuralnetwork.neurons.GrossbergNeuron;
import pl.edu.agh.ki.neuralnetwork.neurons.GrossbergSigmoidalNeuron;
import pl.edu.agh.ki.neuralnetwork.neurons.InnerNeuron;
import pl.edu.agh.ki.neuralnetwork.neurons.InputNeuron;
import pl.edu.agh.ki.neuralnetwork.neurons.InputNeuronImpl;
import pl.edu.agh.ki.neuralnetwork.neurons.KohonenNeuron;
import pl.edu.agh.ki.neuralnetwork.neurons.Neuron;

public class CPNetworkBuilder implements NeuralNetworkBuilder {
	private String path;
	private double minRand;
	private double maxRand;
	private int learingIteartions;
	private boolean sigmoid;
	private int initMode;
	private int colsCount;
	private int rowsCount;
	private int inputLayerSize;
	private int inputRowsCount;
	private int outputLayerSize;
	private KohonenLayer kohonenLayer;
	private GrossbergLayer grossbergLayer;
	private int learningSamplesCount;
	private List<List<Double>> learningInput;
	private List<List<Double>> learningOutput;
	private int toRecogniezeCount;
	private List<List<Double>> toRecognizeInput;
	private SimpleLayer<InputNeuron> inputLayer;
	private int neighbourhood;
	private int neighbourhoodRadius;
	private List<Double> kohonenLearningSpeedList;

	public CPNetworkBuilder(String path) {
		this.path = path;
	}

	/**
	 * function omits line begins with #
	 * 
	 * @param br
	 * @return
	 * @throws IOException
	 */
	private String getNextLine(BufferedReader br) throws IOException {
		String t;
		while ((t = br.readLine()).charAt(0) == '#')
			;
		return t;
	}

	public NeuralNetwork build() throws Exception {
		List<Layer<InnerNeuron>> innerLayers = new LinkedList<Layer<InnerNeuron>>();
		inputLayer = new SimpleLayer<InputNeuron>();

		// reading parameters from file
		System.out.println((new File(path)).getAbsolutePath());
		BufferedReader br = new BufferedReader(new FileReader(new File(path)));
		//read learning speed cooficient count for Kohonen
		int kohonenLearningCooficientCount = Integer.parseInt(getNextLine(br));
		kohonenLearningSpeedList = new ArrayList(kohonenLearningCooficientCount);
		//read learning speed cooficient
		for(int i=0;i<kohonenLearningCooficientCount;i++){
			kohonenLearningSpeedList.add(Double.parseDouble(getNextLine(br)));
		}
		//read learning speed cooficient count for Grossberg
		int grossbergLearningCooficientCount = Integer.parseInt(getNextLine(br));
		List<Double> grossbergLearningSpeedList = new ArrayList(grossbergLearningCooficientCount);
		//read learning speed cooficient
		for(int i=0;i<grossbergLearningCooficientCount;i++){
			grossbergLearningSpeedList.add(Double.parseDouble(getNextLine(br)));
		}
		// read max Rand
		minRand = Double.parseDouble(getNextLine(br));
		// read min Rand
		maxRand = Double.parseDouble(getNextLine(br));
		// sigmoid and delta or linear and widrow-hoff
		sigmoid = Integer.parseInt(getNextLine(br)) == 0 ? false : true;
		// read inputLayerSize
		inputLayerSize = Integer.parseInt(getNextLine(br));
		// read inputColumnsCount
		inputRowsCount = Integer.parseInt(getNextLine(br));
//		System.out.println("Input rows: "+inputRowsCount);
		// read outputLayerSize
		outputLayerSize = Integer.parseInt(getNextLine(br));
		// read kohonenLayer rows count
		rowsCount = Integer.parseInt(getNextLine(br));
		// read kohonen Layer cols count
		colsCount = Integer.parseInt(getNextLine(br));
		// read weights initiation mode
		initMode = Integer.parseInt(getNextLine(br));
		// read neighbourhood mode
		neighbourhood = Integer.parseInt(getNextLine(br));
		//read neighbourhood radius
		//neighbourhoodRadius = Integer.parseInt(getNextLine(br));
		// read learing iterations
		learingIteartions = Integer.parseInt(getNextLine(br));
		System.out.println("learning iterations: "+learingIteartions);
		// read samples count
		learningSamplesCount = Integer.parseInt(getNextLine(br));
		// read learingn input and output
		learningInput = new ArrayList<List<Double>>();
		learningOutput = new ArrayList<List<Double>>();
		for (int j = 0; j < learningSamplesCount; j++) {
			learningInput.add(new ArrayList<Double>(inputLayerSize));

			for (int i = 0; i < inputRowsCount; i++) {
				String[] line = getNextLine(br).split(" ");
				for (String s : line) {
//					System.out.print(s);
					learningInput.get(j).add(Double.parseDouble(s));
				}
//				System.out.println("\n----");
			}
			String []oline = getNextLine(br).split(" ");
			learningOutput.add(new ArrayList<Double>(outputLayerSize));
			for(String s : oline)
				learningOutput.get(j).add(Double.parseDouble(s));
		}
		// read to recognize count
		toRecogniezeCount = Integer.parseInt(getNextLine(br));
		// read to recognize input
		toRecognizeInput = new ArrayList<List<Double>>();
		for (int j = 0; j < toRecogniezeCount; j++) {
			toRecognizeInput.add(new ArrayList<Double>(inputLayerSize));

			for (int i = 0; i < inputRowsCount; i++) {
				String[] line = getNextLine(br).split(" ");
				for (String s : line) {
					toRecognizeInput.get(j).add(Double.parseDouble(s));
				}
			}
		}
		// end of reading parameters

		// creating network

		int kohonenLayerSize = rowsCount * colsCount;
		for (int i = 0; i < inputLayerSize; i++) {
			inputLayer.add(new InputNeuronImpl());
		}

		
		kohonenLayer = new KohonenLayer(rowsCount, colsCount, inputLayerSize,neighbourhood,neighbourhoodRadius);
		grossbergLayer = new GrossbergLayer(sigmoid);
		grossbergLayer.setLearningSpeedList(grossbergLearningSpeedList);
		Random rand = new Random();
		System.out.println("Creating Kohonen layer");
		for (int i = 0; i < kohonenLayerSize; i++) {

			List<Double> weights = new ArrayList<Double>(inputLayerSize);
			if (initMode == 0) {
				for (int j = 0; j < inputLayerSize; j++)
					weights.add(0.0d);
			} else if (initMode == 1) {
				for (int j = 0; j < inputLayerSize; j++){
					double t=rand.nextDouble();
					t=(maxRand-minRand)*t+minRand;
					weights.add(t);
				}
			}
			KohonenNeuron kohonenNeuron = new KohonenNeuron(inputLayer
					.getNeuronsList(), weights);
			kohonenLayer.add(kohonenNeuron);
		}
		kohonenLayer.setNeighbourHood();
		List<Neuron> kohonenNeurons = kohonenLayer.getNeuronsList();

		System.out.println("Creating Grossberg layer");
		for(int i=0; i < outputLayerSize; i++) {
			List<Double> weights = new ArrayList<Double>(inputLayerSize);
//			if (initMode == 0) {
				double initial= Math.sqrt(1.0d/(rowsCount*colsCount));
				for (int j = 0; j < kohonenNeurons.size(); j++)
					weights.add(initial);
//			} else if (initMode == 1) {
//				for (int j = 0; j < kohonenNeurons.size(); j++){
//					double t=rand.nextDouble();
//					t=(maxRand-minRand)*t+minRand;
//					weights.add(t);
//				}
//			}
			InnerNeuron gn = null;
			if(sigmoid)
				gn = new GrossbergSigmoidalNeuron(kohonenNeurons,weights);
			else
				gn = new GrossbergLinearNeuron(kohonenNeurons,weights);
			grossbergLayer.add(gn);
		}
		innerLayers.add(kohonenLayer);
		innerLayers.add(grossbergLayer);
		return new NeuralNetworkImpl(inputLayer, innerLayers);
	}

	/**
	 * funciont says if two neurons are neighbours
	 * 
	 * @param a
	 * @param b
	 * @return 1 for true, 0 for false
	 */
	private int G(KohonenNeuron a, KohonenNeuron b) {
		if (a.isNeighBour(b))
			return 1;
		else
			return 0;
	}

	/**
	 * function returns learning coefficient (it should be decreasing)
	 * 
	 * @param iteration
	 * @return
	 */
//	private double n(int iteration) {
//		if (iteration < 1000)
//			return 1.0;
//		else if (iteration < 2000)
//			return 0.5;
//		else if (iteration < 3000)
//			return 0.25;
//		else if (iteration < 4000)
//			return 0.125;
//		else
//			return 0.062;
//	}
	
	/**
	 * function returns learning coefficient (it should be decreasing)
	 * 
	 * @param iteration
	 * @return
	 */
	private double n(int iteration) {
		int i=iteration/1000;
		if(i>kohonenLearningSpeedList.size()-1)
			i = kohonenLearningSpeedList.size()-1;
		return kohonenLearningSpeedList.get(i);
	}

	/**
	 * function search for a winner
	 * 
	 * @param layer
	 * @param inputVector
	 * @return
	 * @throws WrongArgException
	 */
	private InnerNeuron findWinner(KohonenLayer layer, List<Double> inputVector)
			throws WrongArgException {
		InnerNeuron winner = null;
		double min = Double.MAX_VALUE;
		for (InnerNeuron innerNeuron : layer) {
			((KohonenNeuron) innerNeuron).setDist(inputVector);
		}
		for (InnerNeuron innerNeuron : layer) {
			KohonenNeuron kn = (KohonenNeuron) innerNeuron;
			if (kn.getDist() < min) {
				min = kn.getDist();
				winner = innerNeuron;
			}
		}
		return winner;
	}


	/**
	 * function learns network
	 * 
	 * @param iteration
	 * @throws WrongArgException
	 */
	private void learn(int iteration, List<Double> input, List<Double> output)
			throws WrongArgException {
//		for(double d : input)
//			System.out.print(d+" ");
//		System.out.println("\n--");
		KohonenNeuron winner = (KohonenNeuron) findWinner(kohonenLayer, input);
		for (Neuron neuron : kohonenLayer.getNeuronsList()) {
			KohonenNeuron kn = (KohonenNeuron) neuron;

			for (int i = 0; i < inputLayerSize; i++) {
				try {
					double w = kn.getWeight(inputLayer.get(i));
					double w1 = w + n(iteration) * G(winner, kn)
							* (input.get(i) - w);
					kn.setWeight(inputLayer.get(i), w1);
				} catch (NeuronNotConnectedException e) {
					e.printStackTrace();
				}
			}

		}
		grossbergLayer.learn(iteration, winner, output);
	}
	private List<Double> normalize(List<Double> input){
		List<Double> normalizedList = new ArrayList<Double>(input.size());
//		System.out.println("input size: "+input.size());
		for(int j=0;j<input.size();j++){
			double sum=0.0d;
			for(int i=0;i<input.size();i++){
				sum+=input.get(i)*input.get(i);
//				System.out.println("in("+i+"): "+input.get(i));
			}
//			System.out.println("sum: "+sum);
			sum = Math.sqrt(sum);
//			System.out.println("sqrt(sum): "+sum);
			if(sum != 0)
				normalizedList.add(input.get(j)/sum);
			else
				normalizedList.add(0.0);
		}
		return normalizedList;
	}
	private String recognize(List<Double> input)
			throws WrongArgException {
		for (int i = 0; i < input.size(); i++) {
			if (i % (int) Math.sqrt(input.size()) == 0)
				System.out.print("\n");
			System.out.print(input.get(i) + " ");
			inputLayer.get(i).setInput(input.get(i));
		}
		
		System.out.println();
		StringBuilder sb = new StringBuilder();
//		for(Neuron n : kohonenLayer.getNeuronsList()) {
//			n.compute();
//		}
		for(int i=0; i<outputLayerSize; i++) {
			((GrossbergNeuron)grossbergLayer.get(i)).computeWithWinner(this.findWinner(kohonenLayer, input));
			sb.append(grossbergLayer.get(i).getOutput()+" ");
		}
		sb.append("\n");
		System.out.println("'''findwinner''''");
		System.out.println( this.findWinner(kohonenLayer, input));
//		System.out.println("_________________");
//		System.out.println( this.findWinner(kohonenLayer, input).getOutput());
//		for(Neuron n : kohonenLayer.getNeuronsList())
//			System.out.println("\t"+n.getOutput());
		System.out.println("'''findwinnerend''''");
		return sb.toString();
	}

	public static void main(String[] args) {
		// TODO dopisać elementy dotyczące warstwy grossberga
		CPNetworkBuilder knb = new CPNetworkBuilder("cp.txt");
		try {
			knb.build();
			
			System.out.println("przed uczeniem");
//			System.out.println(knb.kohonenLayer);
			System.out.println("Grossberg: ");
			System.out.println(knb.grossbergLayer);
			System.out.println("---");
			for (int i = 0; i < knb.learingIteartions; i++)
				for (int j = 0; j < knb.learningSamplesCount; j++) 
					knb.learn(i, knb.normalize(knb.learningInput.get(j)), knb.learningOutput.get(j));
			
			System.out.println("po uczeniu");
//			System.out.println(knb.kohonenLayer);
			System.out.println("Grossberg: ");
			System.out.println(knb.grossbergLayer);
			System.out.println("---");
			for (List<Double> input : knb.toRecognizeInput) {
				System.out.println("rozpoznawanie");
				String kn = knb.recognize(knb.normalize(input));
				System.out.println("jako " + kn);

			}
		} catch (Exception e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}

	}

}
